---
title: Built-in Search
description: Built-in document search of Fumadocs
---

Fumadocs supports document search with Orama, It is the default but also the recommended option since it can be self-hosted and totally free.

## Setup

Host the server for handling search requests.

### From Source

Create the server from source object.

<include cwd meta='tab="Next.js" title="app/api/search/route.ts"'>
  ../../examples/next-mdx/app/api/search/route.ts
</include>

<include cwd meta='tab="React Router" title="app/docs/search.ts"'>
  ../../examples/react-router/app/docs/search.ts
</include>

<include cwd meta='tab="Tanstack Start" title="src/routes/api/search.ts"'>
  ../../examples/tanstack-start/src/routes/api/search.ts
</include>

<include cwd meta='tab="Waku" title="src/pages/api/search.ts"'>
  ../../examples/waku/src/pages/api/search.ts
</include>

### From Search Indexes

Create the server from search indexes, each index needs a `structuredData` field.

Usually, it is provided by your content source (e.g. Fumadocs MDX). You can also extract it from Markdown/MDX document using the [Remark Structure](/docs/headless/mdx/structure) plugin.

```ts tab="Next.js" title="app/api/search/route.ts"
import { source } from '@/lib/source';
import { createSearchAPI } from 'fumadocs-core/search/server';

export const { GET } = createSearchAPI('advanced', {
  language: 'english',
  indexes: source.getPages().map((page) => ({
    title: page.data.title,
    description: page.data.description,
    url: page.url,
    id: page.url,
    structuredData: page.data.structuredData,
  })),
});
```

```ts tab="React Router" title="app/docs/search.ts"
import type { Route } from './+types/search';
import { createSearchAPI } from 'fumadocs-core/search/server';
import { source } from '@/lib/source';

const server = createSearchAPI('advanced', {
  language: 'english',
  indexes: source.getPages().map((page) => ({
    title: page.data.title,
    description: page.data.description,
    url: page.url,
    id: page.url,
    structuredData: page.data.structuredData,
  })),
});

export async function loader({ request }: Route.LoaderArgs) {
  return server.GET(request);
}
```

```ts tab="Tanstack Start" title="src/routes/api/search.ts"
import { createFileRoute } from '@tanstack/react-router';
import { source } from '@/lib/source';
import { createSearchAPI } from 'fumadocs-core/search/server';

const server = createSearchAPI('advanced', {
  language: 'english',
  indexes: source.getPages().map((page) => ({
    title: page.data.title,
    description: page.data.description,
    url: page.url,
    id: page.url,
    structuredData: page.data.structuredData,
  })),
});

export const Route = createFileRoute('/api/search')({
  server: {
    handlers: {
      GET: async ({ request }) => server.GET(request),
    },
  },
});
```

```ts tab="Waku" title="src/pages/api/search.ts"
import { source } from '@/lib/source';
import { createSearchAPI } from 'fumadocs-core/search/server';

export const { GET } = createSearchAPI('advanced', {
  language: 'english',
  indexes: source.getPages().map((page) => ({
    title: page.data.title,
    description: page.data.description,
    url: page.url,
    id: page.url,
    structuredData: page.data.structuredData,
  })),
});
```

### Searching Documents

You can search documents using:

- **Fumadocs UI**: Supported out-of-the-box, see [Search UI](/docs/ui/search/orama) for details.
- **Search Client**:

```ts twoslash
import { useDocsSearch } from 'fumadocs-core/search/client';

const client = useDocsSearch({
  type: 'fetch',
});
```

<auto-type-table type='Extract<import("fumadocs-core/search/client").Client, { type: "fetch" }>' />

## Configurations

### Tag Filter

Support filtering results by tag, it's useful for implementing multi-docs similar to this documentation.

```ts
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';

const server = createFromSource(source, {
  buildIndex(page) {
    return {
      title: page.data.title,
      description: page.data.description,
      url: page.url,
      id: page.url,
      structuredData: page.data.structuredData,
      // use your desired value, like page.slugs[0] [!code ++]
      tag: '<value>',
    };
  },
});
```

and update your search client:

- **Fumadocs UI**: Configure [Tag Filter](/docs/ui/search/orama#tag-filter) on Search UI.
- **Search Client**: pass a tag to the hook.

```ts
import { useDocsSearch } from 'fumadocs-core/search/client';

const client = useDocsSearch({
  type: 'fetch',
  tag: '<value>', // [!code ++]
});
```

### Static Mode [#static-export]

To support usage with static site, use `staticGET` from search server and make the route static or pre-rendered.

```ts tab="Next.js" title="app/api/search/route.ts"
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';

// statically cached [!code highlight:2]
export const revalidate = false;
export const { staticGET: GET } = createFromSource(source);
```

```ts tab="React Router" title="app/docs/search.ts"
// make sure this route is pre-rendered in `react-router.config.ts`.
export async function loader() {
  // [!code highlight]
  return server.staticGET();
}
```

```ts tab="Tanstack Start" title="src/routes/api/search.ts"
import { createFileRoute } from '@tanstack/react-router';

export const Route = createFileRoute('/api/search')({
  server: {
    handlers: {
      // [!code highlight]
      GET: async () => server.staticGET(),
    },
  },
});
```

```ts tab="Tanstack Start" title="vite.config.ts"
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    tanstackStart({
      prerender: {
        enabled: true,
      },
      // [!code ++] pre-render the index file
      pages: [{ path: '/api/search' }],
    }),
  ],
});
```

```ts tab="Waku" title="src/pages/api/search.ts"
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';

// [!code highlight]
export const { staticGET: GET } = createFromSource(source);

// statically cached [!code highlight:3]
export const getConfig = async () => ({
  render: 'static',
});
```

> `staticGET` is also available on `createSearchAPI`.

and update your search clients:

- **Fumadocs UI**: use [static client](/docs/ui/search/orama#static) on Search UI.

- **Search Client**: use `static` instead of `fetch`.

  ```ts
  import { useDocsSearch } from 'fumadocs-core/search/client';

  const client = useDocsSearch({
    type: 'static',
  });
  ```

  <auto-type-table type='Extract<import("fumadocs-core/search/client").Client, { type: "static" }>' />

<Callout type='warn' title="Be Careful">

    Static Search requires clients to download the exported search indexes.
    For large docs sites, it can be expensive.

    You should use cloud solutions like Orama Cloud or Algolia for these cases.

</Callout>

## Internationalization

Make sure your language is on the Orama [Supported Languages](https://docs.orama.com/docs/orama-js/supported-languages) list.

```ts title="app/api/search/route.ts" tab="From Source"
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';

const server = createFromSource(source, {
  localeMap: {
    // [locale]: Orama options [!code ++:2]
    ru: { language: 'russian' },
    en: { language: 'english' },
  },
});
```

```ts tab="From Search Indexes"
import { source } from '@/lib/source';
import { createI18nSearchAPI } from 'fumadocs-core/search/server';
import { i18n } from '@/lib/i18n';

const server = createI18nSearchAPI('advanced', {
  i18n, // [!code ++]
  indexes: source.getLanguages().flatMap(({ language, pages }) =>
    pages.map((page) => ({
      title: page.data.title,
      description: page.data.description,
      structuredData: page.data.structuredData,
      id: page.url,
      url: page.url,
      locale: language === 'ru' ? 'russian' : 'english', // [!code ++]
    })),
  ),
});
```

For **Static Mode**, you should configure from client-side instead:

```tsx title="components/search.tsx"
import { useDocsSearch } from 'fumadocs-core/search/client';
import { create } from '@orama/orama';

function initOrama(locale?: string) {
  return create({
    schema: { _: 'string' },
    // [!code ++]
    language: locale === 'ru' ? 'russian' : 'english',
  });
}

function Search() {
  const client = useDocsSearch({
    type: 'static',
    initOrama,
  });

  // ...
}
```

### Special Languages

Chinese and Japanese require additional configurations:

```npm
npm i @orama/tokenizers
```

<include cwd meta='title="app/api/search/route.ts" tab="createFromSource"'>
  ../../examples/i18n/app/api/search/route.ts
</include>

```tsx tab="Static mode" title="components/search.tsx"
import { useDocsSearch } from 'fumadocs-core/search/client';
import { createTokenizer } from '@orama/tokenizers/mandarin';
import { create } from '@orama/orama';

// [!code focus:8]
function initOrama(locale?: string) {
  return create({
    schema: { _: 'string' },
    components: {
      tokenizer: locale === 'cn' ? createTokenizer() : undefined,
    },
  });
}

function Search() {
  const client = useDocsSearch({
    type: 'static',
    initOrama,
  });
  // ...
}
```

and update your search clients:

- **Fumadocs UI**: No changes needed, Fumadocs UI handles this when you have i18n configured correctly.
- **Search Client**:
  Add `locale` to the search client, this will only allow pages with specified locale to be searchable by the user.

```ts
import { useDocsSearch } from 'fumadocs-core/search/client';

const { search, setSearch, query } = useDocsSearch({
  type: 'fetch',
  locale: 'cn',
});
```

## Headless

You can host the search server on other backend such as Express and Elysia.

```ts
import { initAdvancedSearch } from 'fumadocs-core/search/server';

const server = initAdvancedSearch({
  // you still have to pass indexes
});

server.search('query', {
  // you can specify `locale` and `tag` here
});
```
